use crate::command::Command;
use crate::context::Context;

use clap::Args;
use git2::build::{CheckoutBuilder, RepoBuilder};
use git2::{Cred, Error, Progress, RemoteCallbacks};
use git2::{FetchOptions, Repository};
use log::{error, info};
use std::cell::RefCell;
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::time::SystemTime;

/// Git Clone State.
struct State {
  /// Git2 progress.
  progress: Option<Progress<'static>>,
  /// Received chunk.
  recv_chk: usize,
  /// Total chunk.
  total_chk: usize,
  /// Whether newline output is required.
  newline: bool,
  /// Last tick system time.
  last_tick: SystemTime,
  /// Last received total bytes.
  last_recv_bytes: usize,
  /// Last received bytes per second.
  per_recv_bytes: usize,
}

impl State {
  /// Create a git clone state.
  fn new() -> Self {
    Self {
      progress: None,
      recv_chk: 0,
      total_chk: 0,
      newline: false,
      last_tick: SystemTime::now(),
      last_recv_bytes: 0,
      per_recv_bytes: 0,
    }
  }

  /// Print current git clone progress.
  fn print(&mut self) {
    let prog = self.progress.as_ref().unwrap();

    let recv_objs = prog.received_objects();
    let total_objs = prog.total_objects();
    let idx_objs = prog.indexed_objects();
    let kbytes = prog.received_bytes() / 1024;
    let network_pct = 100 * recv_objs / total_objs;
    let index_pct = 100 * idx_objs / total_objs;
    let chunk_pct = if self.total_chk > 0 {
      100 * self.recv_chk / self.total_chk
    } else {
      0
    };

    let elapsed = self.last_tick.elapsed().unwrap().as_secs();

    if elapsed >= 1 {
      self.per_recv_bytes = kbytes - self.last_recv_bytes;
      self.last_recv_bytes = kbytes;
      self.last_tick = SystemTime::now();
    }

    if recv_objs == total_objs
      && idx_objs == total_objs
      && self.total_chk > 0
      && self.recv_chk == self.total_chk
    {
      if !self.newline {
        print!("\r");
        self.newline = true;
      }
      print!(
        "Receiving net: {:3}% ({}/{}) idx: {:3}% ({}/{}) chk: {:3}% ({}/{}) {} kb| {} kb/s, done.\n\
        Resolving deltas: 100% ({}/{}), done.\r",
        network_pct,
        recv_objs,
        total_objs,
        index_pct,
        idx_objs,
        total_objs,
        chunk_pct,
        self.recv_chk,
        self.total_chk,
        kbytes,
        self.per_recv_bytes,
        prog.indexed_deltas(),
        prog.total_deltas()
      );
    } else {
      print!(
        "Receiving net: {:3}% ({}/{}) idx: {:3}% ({}/{}) chk: {:3}% ({}/{}) {} kb | {} kb/s.\r",
        network_pct,
        recv_objs,
        total_objs,
        index_pct,
        idx_objs,
        total_objs,
        chunk_pct,
        self.recv_chk,
        self.total_chk,
        self.last_recv_bytes,
        self.per_recv_bytes
      );
    }
    io::stdout().flush().unwrap();
  }
}

/// Git clone
#[derive(Debug, Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct GitSubCommandsClone {}

impl Command for GitSubCommandsClone {
  // Git clone command
  fn execute(&self, ctx: &Context) {
    let git_config = &ctx.config.as_ref().unwrap().git;
    let url: &str = git_config.repository.url.as_ref();
    let local_path = git_config.local.dir.as_path();

    if local_path.exists() {
      error!(
        "repo: ({}) has been cloned to the ({})",
        url,
        fs::canonicalize(local_path).unwrap().to_str().unwrap()
      );
      return;
    }

    println!("cloing into '{}'...", local_path.to_str().unwrap());

    let state = RefCell::new(State::new());

    // Prepare callbacks.
    let mut cb = RemoteCallbacks::new();
    cb.credentials(|_url, username_from_url, _allowed_types| {
      Cred::ssh_key(
        username_from_url.unwrap(),
        None,
        Path::new(&format!(
          "{}/{}",
          env::var("HOME").unwrap(),
          git_config.ssh.private_key
        )),
        None,
      )
    })
    .transfer_progress(|prog| {
      let mut state = state.borrow_mut();
      state.progress = Some(prog.to_owned());
      state.print();
      true
    });

    // Prepare fetch options.
    let mut fetch_opts = FetchOptions::new();
    fetch_opts.remote_callbacks(cb);

    // Prepare checkout builder.
    let mut checkout = CheckoutBuilder::new();
    checkout.progress(|_path, recv, total| {
      let mut state = state.borrow_mut();
      state.recv_chk = recv;
      state.total_chk = total;
      state.print();
    });

    // Prepare repository builder.
    let mut repo_builder = RepoBuilder::new();

    // Clone repository.
    let repo = repo_builder
      .fetch_options(fetch_opts)
      .with_checkout(checkout)
      .clone(url, local_path)
      .unwrap();
    println!();

    info!(
      "successfully cloned repo: ({}) to ({})",
      url,
      repo.workdir().unwrap().to_str().unwrap()
    );

    match &git_config.repository.branch {
      Some(branch_name) => {
        if branch_name != "master" || branch_name != "main" {
          let branch = repo
            .branch(
              branch_name,
              &repo.head().unwrap().peel_to_commit().unwrap(),
              false,
            )
            .unwrap();
          repo
            .checkout_tree(
              branch.get().peel_to_tree().unwrap().as_object(),
              None,
            )
            .unwrap();
          repo
            .set_head(&("refs/heads/".to_owned() + branch_name))
            .unwrap();

          info!("checkout branch: ({})", branch_name);
        }
      }
      _ => (),
    }

    if let Some(true) = git_config.repository.recursive {
      fn add_subrepos(
        repo: &Repository,
        list: &mut Vec<Repository>,
      ) -> Result<(), Error> {
        for mut subm in repo.submodules()? {
          subm.update(true, None)?;
          info!(
            "successfully updated submodule: ({})",
            subm.path().to_str().unwrap()
          );
          list.push(subm.open()?);
        }
        Ok(())
      }

      println!("updating submodule...");
      let mut repos = Vec::new();
      add_subrepos(&repo, &mut repos).unwrap();
      while let Some(repo) = repos.pop() {
        add_subrepos(&repo, &mut repos).unwrap();
      }
    }
  }
}
